# 优化Unity WebGL的内存

更新时间 2025-02-25

# 内存与崩溃

Unity WebGL 游戏通常比普通 普通小游戏占用更大的内存,在操作系统的控制策略下超出阈值时非常容易被 OOM。 为了提高游戏在中低端机型的稳定性,内存优化极为重要。 由于系统限制,iOS 上我们推荐高性能模,而高性能模式下的内存限制非常严格,如果超出则很有可能被系统终止。相对而言,Android 机型的内存更为宽松。

iOS内存限制:低档机 < 1G, 中高档机 < 1.4G ​

# Unity WebGL 适配小游戏的内存结构

Unity WebGL 内存结构可先参考:

Unity博客:了解 Unity WebGL 中的内存 (Understanding Memory in Unity WebGL) (opens new window)

在小游戏容器里,通常内存占比分布如下: ​

Unity WebGL 是以 WebAssembly + WebGL 技术为基础的应用,运行在浏览器环境,因此游戏内存的分配也是完全托管在这个环境中。

适配在小游戏后,小游戏进程也就成为了“容器”,虽然不再是标准的浏览器,但内存组成结构与上图基本一致,典型游戏的内存占用如下图所示:

  • 基础库+Canvas:在小游戏环境中并不存在DOM,但依然会存在一些基本消耗,比如小游戏公共库,Canvas画布等。典型地,小游戏公共库约占用内存70~90MB,Canvas 画布与设备物理分辨率相关,比如iPhone 11 Pro Max占用约80MB。
  • Unity Heap: 托管堆、本机堆与原生插件底层内存。举例,游戏逻辑分配的C#对象等托管内存、Unity管理的AssetBundle和场景结构等本机内存、第三方原生插件(如lua)调用的malloc分配。
  • WASM编译: 代码编译与运行时指令优化产生的内存,在Android v8、iOS JavascriptCore中还需要大量内存进行JIT优化。所以游戏内存紧张的游戏,在上线前使用平台的分包功能非常必要。
  • GPU内存:纹理或模型Upload GPU之后的显存占用, 由于Unity2021之前不支持ASTC压缩,纹理内存会造成明显膨胀。
  • 其他:
    • 音频:Unity将音频传递给容器(浏览器或小游戏)后,播放音频时将占用的内存。特别地请避免使用fmod播放长音频。
    • Emscripten使用文件系统模拟Linux/POSIX接口,代价是占用与文件同等大小的内存。 请勿使用首资源包、Addressable Cache机制、WWW.LoadFromCacheOrDownload等Cache API
    • 网络请求造成的浏览器端JS临时内存、垃圾回收 ​

# 内存查看工具

我们从大到小各个角度去监控和分析游戏的内存情况:

  • 进程级别:Android Studio、 Xcode Instrument。
  • UnityHeap(CPU主内存):性能面板、Profiling、JavaScript Heap。
  • 引擎与资源:UnityProfiler。 ​

# 进程总内存

查看总内存时,我们需要先确定监控的小游戏进程名称:

  • Android:instant_game0 - instant_game4
  • iOS: WebContent ​ Xcode Instruments (iOS):

使用“Activity Monitor”,选择对应的设备 - all processes - 捕捉,即可看到所有进程的 CPU 与内存情况:

# UnityHeap

UnityHeap 非常关键,典型由以下几部分组成:

  • 托管堆, C#对象托管对象、游戏状态
  • 本机堆, Unity Native 产生,引擎内部对象
  • 原生内存,第三方插件(如lua)直接调用malloc产生

UnityHeap每项指标有三个数值:当前帧、最小值、最大值。

通常而言:MonoHeap + NativeReserverd + 原生插件内存 = DynamicMemory, 因此开发者需要关注这几部分内存。

Unity 引擎视角:

  • MonoHeapReserved:托管堆的内存预留内存
  • MonoHeap:托管堆 (如 C# 业务逻辑)当前的内存使用量
  • NativeReserverd:本机堆 (Native) 内存分配峰值
  • NativeUnused:本机堆 (Native) 空闲内存值
  • NativeAllocated:本机堆 (Native) 当前的内存使用量
  • 注意:第三方原生插件 (如 lua) 分配内存并未呈现,需开发者自行分析。

# Unity Profiler

当发现 UnityHeap (尤其是 Native) 占用比较高时,可通过 UnityProfiler 进一步分析问题所在。关于该工具在小游戏的使用请查阅使用 Unity Profiler 性能调优。

# 内存优化方案

内存计算公式: 小游戏基础库 + Cavnas + 编译内存 + UnityHeap + Gfx显存 + 音频 + JavaScript内存。 UnityHeap = max(托管/Mono内存) + max(Native/Reserved内存 + C原生代码内存)

以 iOS 为例,一款代码 (导出目录 /webgl/Build/xxx.code.unityweb 或 code.wasm) 大小为 30MB 的游戏占用内存为: 小游戏基础库 (100MB) + Cavnas(70MB) + 编译内存 (300MB) + UnityHeap + Gfx 显存 + 音频 + JavaScript (通常 <100MB)。

假如游戏需要支持低档机型,将内存控制到 1G 以内,业务侧 (UnityHeap, Gfx 显存,音频,JavaScript) 需控制在 500MB 左右。我们此处给出转换游戏中最容易遇到的内存问题与解决方案,如果开发者遇到内存问题时请逐个排查优化。 ​

# WASM代码编译内存

  • 问题原因:Unity WebGL将所有代码(引擎、业务代码、第三方插件)编译为跨平台的WebAssembly二进制代码,运行时需进行编译执行。编译所占用内存占用非常大(如在iOS系统,30MB未压缩代码需300MB运行时编译内存)。
  • 解决办法:
    • 使用代码分包工具能降低原编译代码内存50%以上
    • 手动删除多余插件,减少不必要的Unity模块引入(如物理、Unity数据统计等) ​

# GPU内存

  • 问题原因:Unity 2021才开始支持移动平台的压缩纹理,使用RGBA、DXT等纹理格式将导致巨大的内存开销与运行时解压消耗。
  • 解决办法:
    • 使用高性能+模式
    • 升级引擎至2021使用ASTC压缩纹理
    • 关闭HDR,标准渲染管线在"GraphicsSetting-tier2"(WebGL使用tier2),取消勾选"Use HDR",URP管线通过renderer配置取消 ​

# UnityHeap

  • 问题原因:UnityHeap是用于存储所有状态、托管的对象和本机对象,往往由于场景过大或由于业务原因造成瞬间内存峰值。由于Unity WebGL在单帧内无法GC,单帧内瞬间的内存使用非常容易造成crash。同时,Heap是只增不减且存在内存碎片的。游戏应该尽可能的不造成UnityHeap的增长,Heap在不足时的grow行为会导致内存存在尖刺,极易导致内存崩溃
  • 解决办法:
    • 在BuildTool中,设置合适的UnityHeap。(具体数值可以参考测试中的Profiler面板的数值,比如测试中位200MB左右,则可以将UnityHeap设置为256MB)
    • 使用TTAssetBundle加载资源,可以有效的防止UnityHeap的增长
    • 避免场景过大导致瞬间峰值
    • 避免过大的AssetBundle导致瞬间峰值
    • 避免单帧内分配过多的对象, 切忌产生跳跃峰值 ​

# 首资源包与AssetBundle内存

  • 问题原因:首资源包永远占用内存且无法释放;首资源包和AssetBundle自带的cache机制都会使用Emscripten使用的文件系统,应避免使用。
  • 解决办法:
    • 减少首资源包大小,此部分始终占用内存无法释放, 使用AssetBundle
    • AssetBundle按需加载,及时释放以节省内存,AssetBundle使用时被解压占用Unity Native内存,应减少AssetBundle大小,应尽可能使用TTAssetBundle
    • 避免使用Unity自带的文件缓存机制, 首资源包和AssetBundle都不应使用文件Cache
    • 文件访问应使用平台提供的文件系统,不使用系统的File、PlayerPrefs接口 ​

# 音频内存

  • 问题原因:音频将占用小游戏环境的内存
  • 解决办法:
    • 不要使用fmod播放长音频,如游戏BGM
    • 控制音效数量,同时存在的音频数不应该超过20个
    • 尽量强制使用单声道音频,双声道会产生2倍内存消耗 ​

# QA

  • Q: 如何解决iOS出现内存过大导致游戏关闭,常见优化步骤如何?
    • OS由操作系统管理内存上限,在3G RAM机型上限是1.5G,安全内存峰值是1.2-1.3G左右
    • 进程内存离1.5G上限还有较大差距就崩溃了,请检查“UnityHeap预留内存“是否足够
    • 请使用Xcode Instrument查看WebContent进程内存是否在安全范围内
    • 进程内存中的业务内存(UnityHeap, GPU)是每个项目的主要差异点:打开性能面板查看DynamicMemory,峰值不要超过500M;
    • 请务必使用代码分包、压缩纹理(2021以上可使用引擎ASTC)
  • Q: 在Unity Profiler看到内存才200MB+,是否代表游戏内存无问题
    • 不是。游戏占用内存必须以真机环境为准,使用Xcode Instruments测试对应进程的内存占用。
    • Unity Profiler仅能看到Unity Heap相关内存,并不包含小游戏公共库、Cavas、WebAssembly编译以及容器其他内存。
  • Q: 转换面板设置内存值多少合适?
    • 请看前文关于UnityHeap预留内存的说明